using System;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;


namespace Poco
{
	public class UnityNode : AbstractNode
	{
		public static Dictionary<string, string> TypeNames = new Dictionary<string, string>() {
			{ "Text", "Text" },
			{ "Gradient Text", "Gradient.Text" },
			{ "Image", "Image" },
			{ "RawImage", "Raw.Image" },
			{ "Mask", "Mask" },
			{ "2DRectMask", "2D-Rect.Mask" },
			{ "Button", "Button" },
			{ "InputField", "InputField" },
			{ "Toggle", "Toggle" },
			{ "Toggle Group", "ToggleGroup" },
			{ "Slider", "Slider" },
			{ "ScrollBar", "ScrollBar" },
			{ "DropDown", "DropDown" },
			{ "ScrollRect", "ScrollRect" },
			{ "Selectable", "Selectable" },
			{ "Camera", "Camera" },
			{ "RectTransform", "Node" },
		};
		public static string DefaultTypeName = "GameObject";
		private GameObject gameObject;
		private Renderer renderer;
		private RectTransform rectTransform;
		private Rect rect;
		private Vector2 objectPos;
		private List<string> components;
		private Camera camera;


		public UnityNode(GameObject obj)
		{
			gameObject = obj;
			camera = Camera.main;
			foreach (var cam in Camera.allCameras) {
				// skip the main camera
				// we want to use specified camera first then fallback to main camera if no other cameras
				// for further advanced cases, we could test whether the game object is visible within the camera
				if (cam == Camera.main) {
					continue;
				}
				if ((cam.cullingMask & (1 << gameObject.layer)) != 0) {
					camera = cam;
				}
			}

			renderer = gameObject.GetComponent<Renderer>();
			rectTransform = gameObject.GetComponent<RectTransform>();
			rect = GameObjectRect(renderer, rectTransform);
			objectPos = renderer ? WorldToGUIPoint(camera, renderer.bounds.center) : Vector2.zero;
			components = GameObjectAllComponents();
		}

		public override AbstractNode getParent()
		{
			GameObject parentObj = gameObject.transform.parent.gameObject;
			return new UnityNode(parentObj);
		}

		public override List<AbstractNode> getChildren()
		{
			List<AbstractNode> children = new List<AbstractNode>();
			foreach (Transform child in gameObject.transform)
			{
				children.Add(new UnityNode(child.gameObject));
			}
			return children;
		}

		public override object getAttr(string attrName)
		{
			switch (attrName)
			{
				case "name":
					return gameObject.name;
				case "type":
					return GuessObjectTypeFromComponentNames(components);
				case "visible":
					return GameObjectVisible(renderer, components);
				case "pos":
					return GameObjectPosInScreen(objectPos, renderer, rectTransform, rect);
				case "size":
					return GameObjectSizeInScreen(rect, rectTransform);
				case "scale":
					return new List<float>() { 1.0f, 1.0f };
				case "anchorPoint":
					return GameObjectAnchorInScreen(renderer, rect, objectPos);
				case "zOrders":
					return GameObjectzOrders();
				case "clickable":
					return GameObjectClickable(components);
				case "text":
					return GameObjectText();
				case "components":
					return components;
				case "texture":
					return GetImageSourceTexture();
				case "tag":
					return GameObjectTag();
				case "layer":
				    return GameObjectLayerName ();
                case "_ilayer":
                    return GameObjectLayer ();
                case "_instanceId":
					return gameObject.GetInstanceID ();
				default:
					return null;
			}
		}


		public override Dictionary<string, object> enumerateAttrs()
		{
			Dictionary<string, object> payload = GetPayload();
			Dictionary<string, object> ret = new Dictionary<string, object>();
			foreach (KeyValuePair<string, object> p in payload)
			{
				if (p.Value != null)
				{
					ret.Add(p.Key, p.Value);
				}
			}
			return ret;
		}


		private Dictionary<string, object> GetPayload()
		{
			Dictionary<string, object> payload = new Dictionary<string, object>() {
				{ "name", gameObject.name },
				{ "type", GuessObjectTypeFromComponentNames (components) },
				{ "visible", GameObjectVisible (renderer, components) },
				{ "pos", GameObjectPosInScreen (objectPos, renderer, rectTransform, rect) },
				{ "size", GameObjectSizeInScreen (rect, rectTransform) },
				{ "scale", new List<float> (){ 1.0f, 1.0f } },
				{ "anchorPoint", GameObjectAnchorInScreen (renderer, rect, objectPos) },
				{ "zOrders", GameObjectzOrders () },
				{ "clickable", GameObjectClickable (components) },
				{ "text", GameObjectText () },
				{ "components", components },
				{ "texture", GetImageSourceTexture () },
				{ "tag", GameObjectTag () },
				{ "_ilayer", GameObjectLayer() },
				{ "layer", GameObjectLayerName() },
				{ "_instanceId", gameObject.GetInstanceID () },
			};
			return payload;
		}

		private string GuessObjectTypeFromComponentNames(List<string> components)
		{
			List<string> cns = new List<string>(components);
			cns.Reverse();
			foreach (string name in cns)
			{
				if (TypeNames.ContainsKey (name))
				{
					return TypeNames [name];
				}
			}
			return DefaultTypeName;
		}

		private bool GameObjectVisible(Renderer renderer, List<string> components)
		{
			if (gameObject.activeInHierarchy)
			{
				bool light = components.Contains("Light");
				// bool mesh = components.Contains ("MeshRenderer") && components.Contains ("MeshFilter");
				bool particle = components.Contains("ParticleSystem") && components.Contains("ParticleSystemRenderer");
				if (light || particle)
				{
					return false;
				}
				else
				{
					return renderer ? renderer.isVisible : true;
				}
			}
			else
			{
				return false;
			}
		}

		private int GameObjectLayer()
		{
			return gameObject.layer;
		}
		private string GameObjectLayerName()
		{
			return LayerMask.LayerToName (gameObject.layer);
		}

		private bool GameObjectClickable(List<string> components)
		{
			Button button = gameObject.GetComponent<Button>();
			return button ? button.isActiveAndEnabled : false;
		}

		private string GameObjectText()
		{
			Text text = gameObject.GetComponent<Text>();
			return text ? text.text : null;
		}

		private string GameObjectTag()
		{
			string tag;
			try
			{
				tag = !gameObject.CompareTag("Untagged") ? gameObject.tag : null;
			}
			catch (UnityException)
			{
				tag = null;
			}
			return tag;
		}

		private List<string> GameObjectAllComponents()
		{
			List<string> components = new List<string> ();
			Component[] allComponents = gameObject.GetComponents<Component> ();
			if (allComponents != null)
			{
			    foreach (Component ac in allComponents)
				{
				    if (ac != null)
				    {
						components.Add (ac.GetType ().Name);
					}
				}
			}
			return components;
		}

		private Dictionary<string, float> GameObjectzOrders()
		{
			float CameraViewportPoint = 0;
			if (camera != null)
			{
				CameraViewportPoint = Math.Abs(camera.WorldToViewportPoint(gameObject.transform.position).z);
			}
			Dictionary<string, float> zOrders = new Dictionary<string, float>() {
				{ "global", 0f },
				{ "local", -1 * CameraViewportPoint }
			};
			return zOrders;
		}

		private Rect GameObjectRect(Renderer renderer, RectTransform rectTransform)
		{
			Rect rect = new Rect(0, 0, 0, 0);
			if (renderer)
			{
				rect = RendererToScreenSpace(camera, renderer);
			}
			else if (rectTransform)
			{
				rect = RectTransformToScreenSpace(rectTransform);
			}
			return rect;
		}

		private float[] GameObjectPosInScreen(Vector3 objectPos, Renderer renderer, RectTransform rectTransform, Rect rect)
		{
			float[] pos = { 0f, 0f };

			if (renderer)
			{
				// 3d object
				pos[0] = objectPos.x / (float)Screen.width;
				pos[1] = objectPos.y / (float)Screen.height;
			}
			else if (rectTransform)
			{
				// ui object (rendered on screen space, other render modes may be different)
				// use center pos for now
				Canvas rootCanvas = GetRootCanvas (gameObject);
				RenderMode renderMode = rootCanvas != null ? rootCanvas.renderMode : new RenderMode ();
				switch (renderMode)
				{
					case RenderMode.ScreenSpaceCamera:
						//上一个方案经过实际测试发现还有两个问题存在
						//1.在有Canvas Scaler修改了RootCanvas的Scale的情况下坐标的抓取仍然不对，影响到了ScreenSpaceCameram模式在不同分辨率和屏幕比例下识别的兼容性。
						//2.RectTransformUtility转的 rectTransform.transform.position本质上得到的是RectTransform.pivot中心轴在屏幕上的坐标，如果pivot不等于(0.5,0.5)，
						//那么获取到的position就不等于图形的中心点。
						//试了一晚上，找到了解决办法。

                        //用MainCanvas转一次屏幕坐标
                        Vector2 position = RectTransformUtility.WorldToScreenPoint(rootCanvas.worldCamera, rectTransform.transform.position);
                        //注意: 这里的position其实是Pivot点在Screen上的坐标，并不是图形意义上的中心点,在经过下列玄学公式换算才是真的图形中心在屏幕的位置。
                        //公式内算上了rootCanvas.scaleFactor 缩放因子，经测试至少在Canvas Scaler.Expand模式下，什么分辨率和屏幕比都抓的很准，兼容性很强，其他的有待测试。
                        //由于得出来的坐标是左下角为原点，触控输入是左上角为原点，所以要上下反转一下Poco才能用,所以y坐标用Screen.height减去。
                        position.Set(
                            position.x - rectTransform.rect.width * rootCanvas.scaleFactor * (rectTransform.pivot.x - 0.5f),
                            Screen.height - (position.y - rectTransform.rect.height * rootCanvas.scaleFactor * (rectTransform.pivot.y - 0.5f))
                            );
                        pos[0] = position.x / Screen.width;
                        pos[1] = position.y / Screen.height;
                        break;
                    case RenderMode.WorldSpace:
                        Vector2 _pos = RectTransformUtility.WorldToScreenPoint (rootCanvas.worldCamera, rectTransform.transform.position);
                        pos [0] = _pos.x / Screen.width;
                        pos [1] = (Screen.height - _pos.y) / Screen.height;
                        break;
                    default:
                        pos[0] = rect.center.x / (float)Screen.width;
                        pos[1] = rect.center.y / (float)Screen.height;
                        break;
				}
			}
			return pos;
		}

		private Canvas GetRootCanvas (GameObject gameObject)
		{
			Canvas canvas = gameObject.GetComponentInParent<Canvas> ();
			// 如果unity版本小于unity5.5，就用递归的方式取吧，没法直接取rootCanvas
			// 如果有用到4.6以下版本的话就自己手动在这里添加条件吧
#if UNITY_4_6 || UNITY_4_7 || UNITY_4_8 || UNITY_4_9 || UNITY_5_0 || UNITY_5_1 || UNITY_5_2 || UNITY_5_3 || UNITY_5_4
			if (canvas && canvas.isRootCanvas) {
				return canvas;
			} else {
				if (gameObject.transform.parent.gameObject != null) {
					return GetRootCanvas(gameObject.transform.parent.gameObject);
				} else {
					return null;
				}
			}
#else
			if (canvas && canvas.isRootCanvas)
			{
				return canvas;
			}
			else if (canvas)
			{
				return canvas.rootCanvas;
			}
			else
			{
				return null;
			}
#endif
		}

		private float[] GameObjectSizeInScreen(Rect rect, RectTransform rectTransform)
		{
			float[] size = { 0f, 0f };
			if (rectTransform)
			{
				Canvas rootCanvas = GetRootCanvas (gameObject);
				RenderMode renderMode = rootCanvas != null ? rootCanvas.renderMode : new RenderMode ();
				switch (renderMode)
				{
					case RenderMode.ScreenSpaceCamera:
						Rect _rect = RectTransformUtility.PixelAdjustRect (rectTransform, rootCanvas);
						size = new float[] {
                            _rect.width * rootCanvas.scaleFactor / (float)Screen.width,
                            _rect.height * rootCanvas.scaleFactor / (float)Screen.height
                        };
						break;
					case RenderMode.WorldSpace:
						Rect rect_ = rectTransform.rect;
						RectTransform canvasTransform = rootCanvas.GetComponent<RectTransform>();
						size = new float[] { rect_.width / canvasTransform.rect.width, rect_.height / canvasTransform.rect.height };
						break;
					default:
						size = new float[] { rect.width / (float)Screen.width, rect.height / (float)Screen.height };
						break;
				}
			}
			else
			{
				size = new float[] { rect.width / (float)Screen.width, rect.height / (float)Screen.height };
			}
			return size;
		}

		private float[] GameObjectAnchorInScreen(Renderer renderer, Rect rect, Vector3 objectPos)
		{
			float[] defaultValue = { 0.5f, 0.5f };
			if (rectTransform)
			{
				Vector2 data = rectTransform.pivot;
				defaultValue[0] = data[0];
				defaultValue[1] = 1 - data[1];
				return defaultValue;
			}
			if (!renderer)
			{
			    //<Modified> some object do not have renderer
				return defaultValue;
			}
			float[] anchor = { (objectPos.x - rect.xMin) / rect.width, (objectPos.y - rect.yMin) / rect.height };
			if (Double.IsNaN(anchor[0]) || Double.IsNaN(anchor[1]))
			{
				return defaultValue;
			}
			else if (Double.IsPositiveInfinity(anchor[0]) || Double.IsPositiveInfinity(anchor[1]))
			{
				return defaultValue;
			}
			else if (Double.IsNegativeInfinity(anchor[0]) || Double.IsNegativeInfinity(anchor[1]))
			{
				return defaultValue;
			}
			else
			{
				return anchor;
			}
		}

		private string GetImageSourceTexture()
		{
			Image image = gameObject.GetComponent<Image>();
			if (image != null && image.sprite != null)
			{
				return image.sprite.name;
			}

			RawImage rawImage = gameObject.GetComponent<RawImage>();
			if (rawImage != null && rawImage.texture != null)
			{
				return rawImage.texture.name;
			}

			SpriteRenderer spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
			if (spriteRenderer != null && spriteRenderer.sprite != null)
			{
				return spriteRenderer.sprite.name;
			}

			Renderer render = gameObject.GetComponent<Renderer>();
			if (renderer != null && renderer.material != null)
			{
				return renderer.material.color.ToString();
			}

			return null;
		}

		protected static Vector2 WorldToGUIPoint(Camera camera, Vector3 world)
		{
			Vector2 screenPoint = Vector2.zero;
			if (camera != null)
			{
				screenPoint = camera.WorldToScreenPoint(world);
				screenPoint.y = (float)Screen.height - screenPoint.y;
			}
			return screenPoint;
		}

		protected static Rect RendererToScreenSpace(Camera camera, Renderer renderer)
		{
			Vector3 cen = renderer.bounds.center;
			Vector3 ext = renderer.bounds.extents;
			Vector2[] extentPoints = new Vector2[8] {
				WorldToGUIPoint (camera, new Vector3 (cen.x - ext.x, cen.y - ext.y, cen.z - ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x + ext.x, cen.y - ext.y, cen.z - ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x - ext.x, cen.y - ext.y, cen.z + ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x + ext.x, cen.y - ext.y, cen.z + ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x - ext.x, cen.y + ext.y, cen.z - ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x + ext.x, cen.y + ext.y, cen.z - ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x - ext.x, cen.y + ext.y, cen.z + ext.z)),
				WorldToGUIPoint (camera, new Vector3 (cen.x + ext.x, cen.y + ext.y, cen.z + ext.z))
			};
			Vector2 min = extentPoints[0];
			Vector2 max = extentPoints[0];
			foreach (Vector2 v in extentPoints)
			{
				min = Vector2.Min(min, v);
				max = Vector2.Max(max, v);
			}
			return new Rect(min.x, min.y, max.x - min.x, max.y - min.y);
		}

		protected static Rect RectTransformToScreenSpace(RectTransform rectTransform)
		{
			Vector2 size = Vector2.Scale(rectTransform.rect.size, rectTransform.lossyScale);
			Rect rect = new Rect(rectTransform.position.x, Screen.height - rectTransform.position.y, size.x, size.y);
			rect.x -= (rectTransform.pivot.x * size.x);
			rect.y -= ((1.0f - rectTransform.pivot.y) * size.y);
			return rect;
		}

		public static bool SetText (GameObject go, string textVal)
		{
			if (go != null)
			{
				var inputField = go.GetComponent<InputField>();
				if (inputField != null)
				{
					inputField.text = textVal;
					return true;
				}
			}
			return false;
		}
	}
}
